1线程安全性

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class CountExample { public static int clientTotal = 5000 ; public static int threadTotal = 200 ; public static AtomicInteger count = new AtomicInteger(0 ); public static void main (String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0 ;i<clientTotal;i++){ executorService.execute(()->{ try { semaphore.acquire(); add(); semaphore.release(); } catch (InterruptedException e) { log.error("excption" ,e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}" ,count.get()); } private static void add () { count.incrementAndGet(); } }
1 说到原子性,一共有两个方面需要学习一下,一个是JDK中已经提供好的Atomic包,他们均使用了CAS完成线程的原子性操作,另一个是使用锁的机制来处理线程之间的原子性。 锁包括:synchronized、Lock
AtomicInteger 上边的示例代码就是通过AtomicInteger类保证了线程的原子性。
1 那么它是如何保证原子性的呢? 我们接下来分析一下它的源码。 示例中,对count变量的+1操作,采用的是incrementAndGet方法,此方法的源码中调用了一个名为unsafe.getAndAddInt的方法
1 2 3 4 private static void add () { count.incrementAndGet(); count.getAndIncrement(); }
1 2 3 public final int incrementAndGet () { return unsafe.getAndAddInt(this , valueOffset, 1 ) + 1 ; }
1 2 3 4 5 6 7 public final int getAndAddInt (Object var1, long var2, int var4) { int var5; do { var5 = this .getIntVolatile(var1, var2); } while (!this .compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
在此方法中,方法参数为要操作的对象Object var1、期望底层当前的数值为var2、要修改的数值var4。 定义的var5为真正从底层取出来的值。 采用do.. while循环的方式去获取底层数值并与期望值进行比较,比较成功才将值进行修改。 而这个比较再进行修改的方法就是compareAndSwapInt就是我们所说的CAS,它是一系列的接口,比如下面罗列的几个接口。 使用native修饰,是底层的方法。 CAS取的是compareAndSwap三个单词的首字母.
1 public final native boolean compareAndSwapInt (Object var1, long var2, int var4, int var5) ;
JNI在某些情况下可能带来很大的开销和性能损失:
调用 JNI 方法是很笨重的操作, 特别是在多次重复调用的情况下.
Native 方法不会被 JVM 内联, 也不会被 JIT compiled 优化 , 因为方法已经被编译过了.
Java 数组可能会被拷贝一份,以传递给 native 方法, 执行完之后再拷贝回去. 其开销与数组的长度是线性相关的.
如果传递一个对象给方法,或者需要一个回调,那么 Native 方法可能会自己调用JVM。 访问Java对象的属性、方法和类型时, Native代码需要类似reflection的东西。签名由字符串指定,通从JVM中查询。这非常缓慢并且容易出错。
Java 中的字符串(String) 也是对象, 有 length 属性,并且是编码过的. 读取或者创建字符串都需要一次时间复杂度为 O(n) 的复制操作.
再分析其函数的调用:
1 2 3 4 5 6 7 8 public final int getAndAddInt (Object var1, long var2, int var4) { int var5; do { var5 = this .getIntVolatile(var1, var2); } while (!this .compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
其中var1为当前传入的对象即count,var2是当前count的值,var4即为incrementAndGet()方法的默认加数,为1,var5是调用的另一个JNI的方法得到底层当前的count值。
1 public native int getIntVolatile (Object var1, long var2) ;
Volatile关键字很熟悉,简述是保证了线程之间操作的对象的值互相透明、可见性。**
(Volatile关键字的详述又该是一个大文章了!过阵子再写!) 那么现在应该就懂了,用底层得到的值不断循环比较当前的值,如果两者相等就加一并再用JNI底层方法写入主内存。 AtomicInteger类就是使用了该方法即CAS原理 来保证线程安全的。(CAS原理也是一个大块头。。) 同理,AtomicXXX类(XXX是属于Java的Object,例如Long、Double等)都是该方法保证的!
ConpareAndSet概述
该方法和ConpareAndSwap有些相似,但还有不同:ConpareAndSet(a,b)更多的是用在AtomicBoolean类中(Boolean要求false与true的转换同一时间只能被一个线程调用,且false(true)只能转换为true(false),其中的参数a,b即为只能相互转换的数,顺序为:当为a的时候更新为b )。
longAdder 1 2 3 4 public static LongAdder longAdder = new LongAdder(); longAdder.increment();
那么问题来了,为什么有了AtomicLong还要新增一个LongAdder呢? 原因是:CAS底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。 如果竞争不激烈的时候,修改成功率很高,否则失败率很高。 在失败的时候,这些重复的原子性操作会耗费性能。
知识点: 对于普通类型的long、double变量,JVM允许将64位的读操作或写操作拆成两个32位的操作。
LongAdder类的实现核心是将热点数据分离,比如说它可以将AtomicLong内部的内部核心数据value分离成一个数组,每个线程访问时,通过hash等算法映射到其中一个数字进行计数, 而最终的计数结果则为这个数组的求和累加,其中热点数据value会被分离成多个单元的cell,每个cell独自维护内部的值。 当前对象的实际值由所有的cell累计合成,这样热点就进行了有效地分离,并提高了并行度。 这相当于将AtomicLong的单点的更新压力分担到各个节点上。 在低并发的时候通过对base的直接更新,可以保障和AtomicLong的性能基本一致。 而在高并发的时候通过分散提高了性能。
1 2 3 4 5 6 7 由于AtomicLong的底层是CAS原理,即通过死循环不断地比较当前值与主内存的值,那么当线程并发量较少时其比较成功的概率是高的,但放并发量很大时,比较成功概率就会很低,越低则死循环持续的时间越久,占用的系统资源越大,系统运行的效率越低。 LongAdder的实现思想为:热点数据分离:将AtomicLong的核心数据value分离为一个数组,每个线程访问时通过hash等算法预测到其中的一个数字进行计数,最终的计数结果为该数组各部分的求和。热点数据value会被分为多个部分的shell,每个shell独自维护各自的值,而当前对象的实际值为各部分shell的累加和。保证热点数据的有效分离,提高并行度。LongAdder类实际是在AtomicLong的基础上进行了优化:在低并发时不做分离,同AtomicLong是对base的直接CAS更新,但是高并发时将单点的压力分摊到各部分的shell上以提高性能。
缺点:如果在统计的时候,如果有并发更新,可能会有统计数据有误差。 实际使用中在处理高并发计数的时候优先使用LongAdder,而不是AtomicLong在线程竞争很低的时候,使用AtomicLong会简单效率更高一些。 比如序列号生成(准确性
AtomicReference 1 2 3 4 5 6 7 8 9 10 11 @Slf 4jpublic class AtomicExample { private static AtomicReference<Integer> count = new AtomicReference<>(0 ); public static void main (String[] args) { count.compareAndSet(0 , 2 ); count.compareAndSet(0 , 1 ); count.compareAndSet(1 , 3 ); count.compareAndSet(2 , 4 ); count.compareAndSet(3 , 5 ); log.info("count={}" , count); }
AtomicIntegerFieldUpdater
AtomicReferenceFieldUpdater的核心为:原子性地去更新某一个类的实例中指定的某一个字段,而该字段必须使用volatile标识且不能被static标识进行声明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Slf 4j@ThreadSafe public class AtomicExample5 { private static AtomicIntegerFieldUpdater<AtomicExample5> updater = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count" ); @Getter public volatile int count = 100 ; public static void main (String[] args) { AtomicExample5 example5 = new AtomicExample5(); if (updater.compareAndSet(example5, 100 , 120 )) { log.info("update success 1, {}" , example5.getCount()); } if (updater.compareAndSet(example5, 100 , 120 )) { log.info("update success 2, {}" , example5.getCount()); } else { log.info("update failed, {}" , example5.getCount()); } } }

$ 手记小总结$
AtomicReference与AtomicReferenceFieldUpdater都是用于原子性地修改更新,都是线程安全的。 但是后者是通过指定类的实例中的某一个字段,且该字段必须使用volatile标识且非static标识进行声明。 这两个类实际中用到的都不是很多…
AtomicStampReference与CAS的ABA问题
CAS算法实现一个重要前提:需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
^_^什么是ABA问题?
A最开始的内存地址是X,然后失效了,有分配了B,恰好内存地址是X,这时候通过CAS操作也成功了 ,但是在像Java这种有GC机制的语言中,若A失效就被GC处理了,不会发生这种情况。 若是在像C/C++没有GC机制的语言中是有可能出现的。
像各种乐观锁的实现中通常都会用版本戳version(保持递增加一规则)来对记录或对象标记,在CAS时加上对该版本戳的比较。
在Java语言中,AtomicStampedReference也是使用类似的机制做解决方案。
其核心方法为
其中设定stamp值的原子性方法为:由于是非阻塞方法,即使两个参数对应,请求也会不合逻辑地失败。 但最终当没有其他线程请求时就会成功。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public boolean compareAndSet (V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
AtomicStampReference的处理思想是,每次变量更新的时候,将变量的版本号+1,之前的ABA问题中,变量经过两次操作以后,变量的版本号就会由1变成3,也就是说只要线程对变量进行过操作, 变量的版本号就会发生更改。 从而解决了ABA问题。
解释一下上边的源码: 类中维护了一个volatile修饰的Pair类型变量current,Pair是一个私有的静态类,current可以理解为底层数值。 compareAndSet方法的参数部分分别为期望的引用、新的引用、期望的版本号、新的版本号。
return的逻辑为判断了期望的引用和版本号是否与底层的引用和版本号相符,并且排除了新的引用和新的版本号与底层的值相同的情况(即不需要修改)的情况(return代码部分3、4行)。 条件成立,执行casPair方法,调用CAS操作。
AtomicLongArray 这个类实际上维护了一个Array数组,我们在对数值进行更新的时候,会多一个索引值让我们更新。
原子性,提供了互斥访问,同一时刻只能有一个线程来对它进行操作。 那么在java里,保证同一时刻只有一个线程对它进行操作的,除了Atomic包之外,还有锁的机制。 JDK提供锁主要分为两种:synchronized和Lock。 接下来我们了解一下synchronized。
AtomicBoolean 这个类中值得一提的是它包含了一个名为compareAndSet的方法,这个方法可以做到的是控制一个boolean变量在一件事情执行之前为false,事情执行之后变为true。 或者也可以理解为可以控制某一件事只让一个线程执行,并仅能执行一次。 他的源码如下
1 2 3 4 5 6 7 public final boolean compareAndSet(boolean expect, boolean update) { int e = expect ? 1 : 0; int u = update ? 1 : 0; return unsafe.compareAndSwapInt(this, valueOffset, e, u); } --------------------- !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 //是否发生过 private static AtomicBoolean isHappened = new AtomicBoolean(false); // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); test(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("isHappened:{}", isHappened.get()); } private static void test() { if (isHappened.compareAndSet(false, true)) {//控制某有一段代码只执行一次 log.info("execute"); } }
2 原子性-synchronized
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Slf 4jpublic class SynchronizedExample1 { public void test1 (int j) { synchronized (this ) { for (int i = 0 ; i < 10 ; i++) { log.info("test1 {} - {}" , j, i); } } } public synchronized void test2 (int j) { for (int i = 0 ; i < 10 ; i++) { log.info("test2 {} - {}" , j, i); } } public static void main (String[] args) { SynchronizedExample1 example1 = new SynchronizedExample1(); SynchronizedExample1 example2 = new SynchronizedExample1(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> { example1.test2(1 ); }); executorService.execute(() -> { example2.test2(2 ); }); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Slf4j public class SynchronizedExample2 { public static void test1 (int j) { synchronized (SynchronizedExample2.class) { for (int i = 0 ; i < 10 ; i++) { log.info("test1 {} - {}" , j, i); } } } public static synchronized void test2 (int j) { for (int i = 0 ; i < 10 ; i++) { log.info("test2 {} - {}" , j, i); } } public static void main (String[] args) { SynchronizedExample2 example1 = new SynchronizedExample2(); SynchronizedExample2 example2 = new SynchronizedExample2(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> { example1.test1(1 ); }); executorService.execute(() -> { example2.test1(2 ); }); } }
概述 Java 语言中主要的同步手段就是 synchronized 关键字,它强制实行互斥,确保执行 synchronized 块的线程的动作,能够被后来执行受相同锁保护的 synchronized 块的其他线程看到。 在使用得当的时候,内在锁可以让程序做到线程安全,但是在使用锁定保护短的代码路径,而且线程频繁地争用锁的时候,锁定可能成为相当繁重的操作。参考学习
内在锁synchronized关键字的实现依赖于JVM,因此在这个关键字作用的对象的作用范围内都是同一时刻只能有一个线程进行操作。
它是一种同步锁,作用对象有四种:
修饰代码块:指用大括号括起来的代码,被称为“同步代码块”,作用于调用该代码块的对象。
修饰方法:指整个方法,被称为同步方法,作用于调用该方法的对象。
修饰静态方法:指整个静态方法,由于静态方法可以被理解为类级别的对象,因此它作用于该类的所有的对象。
修饰类:指整个类,作用于该类的所有的对象。
引申拓展:另一种锁Lock JDK提供的基于源码的锁,依赖于特殊的CPU指令、代码实现。其代表为ReentrantLock. 后续将会单独做一篇手记。
线程安全中原子性方式的简单对比
synchronized:是不可中断锁,适合竞争不激烈的场景,其可读性较好。
Lock:可通过unlock进行锁的中断,在竞争激烈时仍可以保持常态,同步多样化。
Atomic包:竞争激烈时也可以保持常态,性能比Lock好,但缺点是只能通过CAS同步一个值。
https://www.jianshu.com/p/d53bf830fa09
https://blog.csdn.net/zjy15203167987/article/details/82531772
3 可见vcolatile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @Slf 4j@NotThreadSafe public class CountExample4 { public static int clientTotal = 5000 ; public static int threadTotal = 200 ; public static volatile int count = 0 ; public static void main (String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0 ; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception" , e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}" , count); } private static void add () { count++; } } 在单线程执行此操作绝对没有问题,但是在多线程环境中,假设有两个线程A、B同时执行count++操作,某一刻A与B同时读取主存中count的值,然后在自己线程对应的工作空间中对count+1 ,最后又同时将count+1 的值写回主存。到此,count+1 的值被写回主存两遍,所以导致最终的count值小了1 。在整体程序执行过程中,该事件发生一次或多次,自然结果就不正确。 那么volatile 适合做什么呢?其实它比较适合做状态标记量(不会涉及到多线程同时读写的操作),而且要保证两点: (1 )对变量的写操作不依赖于当前值 (2 )该变量没有包含在具有其他变量的不变的式子中 例如: volatile boolean inited = false ; context = loadContext(); inited = true ; while (!inited){ sleep(); } doSomethingWithConfig(context);
Volatile概述 指一个线程对主内存的修改,可以及时地被其他线程观察到。
导致共享变量在线程间不可见的原因有:
多线程交叉进行。
重排序结合多线程运行。
共享变量更新后的值未及时进行工作内存与主内存的更新。
Java内存模型对synchronized关键字有两条规定。
线程解锁前必须把共享变量的最新值刷新到主内存。
线程加锁时,将清空工作内存中共享变量的值。那么工作内存需要时只能从工作内存中取值。
工作原理:此时的加锁和解锁用的是一个锁,即多线程之间共享synchronized一个锁。
可见性之volatile关键字 工作原理:通过加入内存屏障和禁止重排序优化来实现,即通过内存屏障保证了禁止重排序优化 内存屏障从以下两点进行说明:
对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将本地工作内存中的值刷新到主内存。
对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存读取共享变量的值。
即强迫从主内存中的共享变量进行交互。
** volatile使用的场景
场景需具备的条件:
对变量的写操作不依赖与当前值
该变量没有包含在具有其他变量的不变的式子中。
因此,
volatile适合作为状态标记量(boolean型)
适用于doublecheck场景。 (例子会在之后手记中添上……)
**
4 有序性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 java中保证有序性 java提供了 volatile、synchronized、Lock可以用来保证有序性 另外,java内存模型具备一些先天的有序性,即不需要任何手段就能得到保证的有序性。通常被我们成为happens-before原则(先行发生原则)。如果两个线程的执行顺序无法从happens-before原则推导出来,那么就不能保证它们的有序性,虚拟机就可以对它们进行重排序。 【以下规则来自于《深入理解java虚拟机》】 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作 volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作(重要) 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C —————————————————————————————————————————————— 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
问题导出
Java内存模型中,允许编译器和翻译器对指令进行重排序,但是重排序的过程不会影响到单线程 程序的运行,却会影响到多线程并发执行的正确性。
Java中的volatile通过内存屏障也可以一定程度上禁止指令的重排序。
synchronized和Lock机制都是通过线程阻塞对于主内存来讲其同一时刻只有一个线程可以访问操作,保证了有序性。
Java内存模型实际存在先天的有序性:不需要通过任何手段即可保证一定的有序性,即happens-before原则。
如果两个操作的次序无法从happens-before原则中得到保证,那么该两个操作不能保证自己的有序性:即被JVM随意地进行重排序。
happens-before原则
前四条较为重要,后四条浅显易懂
程序次序规则:单线程内 ,按照代码顺序,书写在前面的操作先行发生与书写在后面的操作。(此时的JVM只会对非数据依赖的指令进行重排序)
锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
传递规则:若操作A先行发生于操作B,B又先行发生于操作C,那么操作A先行发生于操作C。
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。